package com.keguoyu.easymvp.compiler;

import java.io.IOException;
import java.io.Writer;
import java.lang.annotation.Annotation;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedOptions;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;
import javax.tools.Diagnostic;
import javax.tools.FileObject;
import javax.tools.StandardLocation;

import com.google.auto.service.AutoService;
import com.google.gson.Gson;
import com.keguoyu.easymvp.annotation.InvokeBy;
import com.keguoyu.easymvp.annotation.Plugin;
import com.keguoyu.easymvp.annotation.Singleton;
import com.keguoyu.easymvp.commons.PluginFactory;
import com.keguoyu.easymvp.compiler.base.BaseProcessor;
import com.squareup.javapoet.AnnotationSpec;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;

@AutoService(Processor.class)
@SupportedOptions({PluginProcessor.GENERATE_PKG_NAME})
public class PluginProcessor extends BaseProcessor {
  static final String GENERATE_PKG_NAME = "generatePkg";
  private static final String DEFAULT_FILE_NAME = "META-INF/services/plugin/instance_info";
  private final Set<String> mProcessClassNames = new HashSet<>();
  private Writer mWriter;

  @Override
  public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
    parsePlugin(roundEnvironment);
    return false;
  }

  private Writer openWriter() {
    if (mWriter != null) {
      return mWriter;
    }
    FileObject source = null;
    try {
      source = mFiler.createResource(StandardLocation.CLASS_OUTPUT, "",
          DEFAULT_FILE_NAME);
      mWriter = source.openWriter();
      mMessager.printMessage(Diagnostic.Kind.NOTE, source.getName());
    } catch (Exception e) {
      e.printStackTrace();
    }
    return mWriter;
  }

  private static List<ClassVisitor> sClassVisitors = Arrays.asList(
      new PluginClassVisitor(),
      new SingletonClassVisitor()
  );

  private void parsePlugin(RoundEnvironment roundEnvironment) {
    HashMap<String, String> clsMap = new HashMap<>();
    for (ClassVisitor classVisitor : sClassVisitors) {
      Set<? extends Element> elements =
          roundEnvironment.getElementsAnnotatedWith(classVisitor.getAnnotationClass());
      elements.forEach((Consumer<Element>) element -> generateClass(classVisitor, element, clsMap));
    }
    if (!clsMap.isEmpty()) {
      Writer writer = openWriter();
      try {
        Gson gson = new Gson();
        String json = gson.toJson(clsMap);
        writer.append(json);
        writer.append("\n");
        writer.flush();
        mMessager.printMessage(Diagnostic.Kind.NOTE, json);
      } catch (IOException ignored) {
        mMessager.printMessage(Diagnostic.Kind.ERROR, "fail");
      }
    }
  }

  private void generateClass(ClassVisitor classVisitor, Element rootClass, HashMap<String, String> clsMap) {
    if (rootClass == null || rootClass.getKind() != ElementKind.CLASS) {
      return;
    }
    TypeMirror interfaceName = classVisitor.getTypeMirror((TypeElement) rootClass);
    Element interfaceElement = mTypeUtils.asElement(interfaceName);
    //放到json文件中
    clsMap.putIfAbsent(((TypeElement) interfaceElement).getQualifiedName().toString(),
        ((TypeElement) rootClass).getQualifiedName().toString());
    String className = rootClass.getSimpleName().toString() + "Factory";
    if (!mProcessClassNames.add(className)) {
      return;
    }
    TypeName rootType = TypeName.get(rootClass.asType());
    TypeSpec.Builder factory =
        TypeSpec.classBuilder(className)
            .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
            .addAnnotation(mAutoGenerated)
            .superclass(ParameterizedTypeName.get(ClassName.get(PluginFactory.class),
                rootType));

    MethodSpec.Builder newInstance = MethodSpec.methodBuilder("createInstance")
        .addModifiers(Modifier.PUBLIC)
        .returns(rootType);
    newInstance.addStatement("return new $T()", rootType);
    MethodSpec.Builder register = MethodSpec.methodBuilder("register")
        .addAnnotation(classVisitor.getInvokeBy())
        .addModifiers(Modifier.PUBLIC, Modifier.FINAL, Modifier.STATIC);
    classVisitor.onClass(factory, rootClass, className, interfaceName, register);
    factory
        .addMethod(newInstance.build())
        .addMethod(register.build());
    String pkg = mElementUtils.getPackageOf(rootClass).toString();
    // 输出Factory.java文件
    writeClass(pkg, className, factory);
  }

  @Override
  public Set<String> getSupportedAnnotationTypes() {
    Set<String> set = new HashSet<>();
    set.add(Plugin.class.getName());
    set.add(Singleton.class.getName());
    return set;
  }

  @Override
  public SourceVersion getSupportedSourceVersion() {
    return SourceVersion.RELEASE_8;
  }

  private interface ClassVisitor {

    Class<? extends Annotation> getAnnotationClass();

    TypeMirror getTypeMirror(TypeElement typeElement);

    AnnotationSpec getInvokeBy();

    void onClass(TypeSpec.Builder typeSpec, Element rootClass, String className, TypeMirror interfaceName, MethodSpec.Builder register);
  }

  private static class PluginClassVisitor implements ClassVisitor {
    private static final ClassName sPluginConfig =
        ClassName.get("com.keguoyu.okmvp.ioc", "PluginConfig");
    private static final AnnotationSpec sInvokeBy =
        AnnotationSpec.builder(InvokeBy.class)
            .addMember("invokerClass", "$T.class", sPluginConfig)
            .addMember("methodId", "$L", CodeBlock.of("$T.INVOKER_ID", sPluginConfig))
            .build();

    @Override
    public Class<? extends Annotation> getAnnotationClass() {
      return Plugin.class;
    }

    @Override
    public TypeMirror getTypeMirror(TypeElement typeElement) {
      List<? extends TypeMirror> interfaces = typeElement.getInterfaces();
      return interfaces.size()  == 1 ? interfaces.get(0) : null;
    }

    @Override
    public AnnotationSpec getInvokeBy() {
      return sInvokeBy;
    }

    @Override
    public void onClass(TypeSpec.Builder factory, Element rootClass, String className, TypeMirror interfaceName,
        MethodSpec.Builder register) {
      Plugin plugin = rootClass.getAnnotation(Plugin.class);
      register.addStatement("$T.register($T.class, new $L(), $L)", sPluginConfig, interfaceName, className, plugin.priority());
    }
  }

  private static class SingletonClassVisitor implements ClassVisitor {
    private static final ClassName sSingletonConfig =
        ClassName.get("com.keguoyu.okmvp.ioc", "SingletonConfig");
    private static final AnnotationSpec sInvokeBy =
        AnnotationSpec.builder(InvokeBy.class)
            .addMember("invokerClass", "$T.class", sSingletonConfig)
            .addMember("methodId", "$L", CodeBlock.of("$T.INVOKER_ID", sSingletonConfig))
            .build();

    @Override
    public Class<? extends Annotation> getAnnotationClass() {
      return Singleton.class;
    }

    @Override
    public TypeMirror getTypeMirror(TypeElement typeElement) {
      return typeElement.asType();
    }

    @Override
    public AnnotationSpec getInvokeBy() {
      return sInvokeBy;
    }

    @Override
    public void onClass(TypeSpec.Builder typeSpec, Element rootClass, String className,
        TypeMirror interfaceName, MethodSpec.Builder register) {
      Plugin plugin = rootClass.getAnnotation(Plugin.class);
      register.addStatement("$T.register($T.class, new $L())", sSingletonConfig, interfaceName, className);
    }
  }

}
